﻿using System;
using System.Collections;

namespace taurus.client
{
    /// <summary>
    /// 
    /// </summary>
    public class TDataSerializer
    {
        private static TDataSerializer instance;
        internal static TDataSerializer me()
        {
            if (instance == null)
            {
                instance = new TDataSerializer();
            }
            return instance;
        }

        private TDataSerializer()
        {
        }

        internal ByteArray array2Binary(ITArray array)
        {
            ByteArray buffer = new ByteArray();
            buffer.writeByte(Convert.ToByte(TDataType.TARRAY));
            buffer.writeShort(Convert.ToInt16(array.size()));
            return arr2bin(array, buffer);
        }

        internal ByteArray object2Binary(ITObject obj)
        {
            ByteArray buffer = new ByteArray();
            buffer.writeByte(Convert.ToByte(TDataType.TOBJECT));
            buffer.writeShort(Convert.ToInt16(obj.size()));
            return obj2bin(obj, buffer);
        }

        public string object2json(ITObject obj)
        {
            Hashtable map = new Hashtable();
            flattenObject(map, obj);
            return Json.Serialize(map);
        }

        public string array2json(ITArray arr)
        {
            var list = new ArrayList();
            flattenArray(list, arr);
            return Json.Serialize(list);
        }

        private void flattenObject(Hashtable map, ITObject obj)
        {
            var keys = obj.getKeys();
            foreach(string key in keys)
            {
                TDataWrapper value = obj.get(key);
                if (value.getTypeId() == TDataType.TOBJECT)
                {
                    var newMap = new Hashtable();
                    map[key] = newMap;
                    this.flattenObject(newMap,(ITObject) value.getObject());
                }
                else if (value.getTypeId() == TDataType.TARRAY)
                {
                    var newList = new ArrayList();
                    map[key] = newList;
                    this.flattenArray(newList, (ITArray)value.getObject());
                }
                else
                {
                    map[key] = value.getObject();
                }
            }
        }

        private void flattenArray(ArrayList array, ITArray tarray)
        {
            for(int i = 0; i < tarray.size(); ++i)
            {
                TDataWrapper value = tarray.get(i);
                if (value.getTypeId() == TDataType.TOBJECT)
                {
                    var newMap = new Hashtable();
                    array.Add(newMap);
                    this.flattenObject(newMap, (ITObject)value.getObject());
                }
                else if (value.getTypeId() == TDataType.TARRAY)
                {
                    var newList = new ArrayList();
                    array.Add(newList);
                    this.flattenArray(newList, (ITArray)value.getObject());
                }
                else
                {
                    array.Add(value.getObject());
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="jsonStr"></param>
        /// <returns></returns>
        public ITObject json2object(string jsonStr)
        {
            if (jsonStr.Length < 2)
            {
                throw new Exception("Can't decode Object. JSON String is too short. Len: " + jsonStr.Length);
            }
            Hashtable o = (Hashtable)Json.Deserialize(jsonStr);
            return decodeTObject(o);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="jsonStr"></param>
        /// <returns></returns>
        public ITArray json2array(String jsonStr)
        {
            if (jsonStr.Length < 2)
            {
                throw new Exception("Can't decode Object. JSON String is too short. Len: " + jsonStr.Length);
            }
            ArrayList jsa = (ArrayList)Json.Deserialize(jsonStr);
            return decodeTArray(jsa);
        }

        private ITArray decodeTArray(ArrayList jsa)
        {
            ITArray array = new TArrayLite();

            foreach (object value in jsa)
            {
                TDataWrapper decodedObject = decodeJsonObject(value);
                if (decodedObject != null)
                    array.add(decodedObject);
                else
                {
                    throw new Exception("(json2sfarray) Could not decode value for object: " + value);
                }
            }
            return array;
        }

        private ITObject decodeTObject(Hashtable jso)
        {
            ITObject o = new TObjectLite();

            foreach (DictionaryEntry entry in jso)
            {
                var key = entry.Key;
                var value = entry.Value;
                TDataWrapper decodedObject = decodeJsonObject(value);
                if (decodedObject != null)
                    o.put((string)key, decodedObject);
                else
                {
                    throw new Exception("(json2tobj) Could not decode value for key: " + key);
                }
            }
            return o;
        }


        private TDataWrapper decodeJsonObject(Object o)
        {

            if ((o is int))
            {
                return new TDataWrapper(TDataType.INT, o);
            }
            if ((o is long))
            {
                return new TDataWrapper(TDataType.LONG, o);
            }
            if ((o is double))
            {
                return new TDataWrapper(TDataType.DOUBLE, o);
            }
            if ((o is bool))
            {
                return new TDataWrapper(TDataType.BOOL, o);
            }
            if ((o is string))
            {
                return new TDataWrapper(TDataType.STRING, o);
            }
            if ((o is Hashtable))
            {
                var jso = (Hashtable)o;
                return new TDataWrapper(TDataType.TOBJECT, decodeTObject(jso));
            }
            if ((o is ArrayList))
            {
                return new TDataWrapper(TDataType.TARRAY, decodeTArray((ArrayList)o));
            }

            throw new Exception(string.Format("Unknown DataType! {0}", (o == null ? "null" : o.GetType().ToString())));
        }
        internal ITArray binary2Array(ByteArray data)
        {
            if (data.length < 3)
            {
                throw new Exception("Can't decode an Array. Byte data is insufficient. Size: " + data.length + " byte(s)");
            }
            data.position = 0;
            return decodeTArray(data);
        }

        internal ITObject binary2Object(ByteArray data)
        {
            if (data.length < 3)
            {
                throw new Exception("Can't decode an Object. Byte data is insufficient. Size: " + data.length + " byte(s)");
            }
            data.position = 0;
            return decodeTObject(data);
        }

        private ByteArray addData(ByteArray buffer, ByteArray newData)
        {
            buffer.writeBytes(newData.bytes);
            return buffer;
        }

        private ByteArray arr2bin(ITArray array, ByteArray buffer)
        {
            for (int i = 0; i < array.size(); i++)
            {
                TDataWrapper wrapped = array.get(i);
                buffer = encodeObject(buffer, wrapped.getTypeId(), wrapped.getObject());
            }
            return buffer;
        }

        private TDataWrapper binDecode_BOOL(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.BOOL, buffer.readBool());
        }

        private TDataWrapper binDecode_BYTE(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.BYTE, buffer.readByte());
        }

        private TDataWrapper binDecode_BYTE_ARRAY(ByteArray buffer)
        {
            int count = buffer.readInt();
            if (count < 0)
            {
                throw new Exception("Array negative size: " + count);
            }
            return new TDataWrapper(TDataType.BYTE_ARRAY, buffer.readBytes(count));
        }

        private TDataWrapper binDecode_DOUBLE(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.DOUBLE, buffer.readDouble());
        }

        private TDataWrapper binDecode_FLOAT(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.FLOAT, buffer.readFloat());
        }

        private TDataWrapper binDecode_INT(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.INT, buffer.readInt());
        }

        private TDataWrapper binDecode_LONG(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.LONG, buffer.readLong());
        }

        private TDataWrapper binDecode_NULL(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.NULL, null);
        }

        private TDataWrapper binDecode_SHORT(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.SHORT, buffer.readShort());
        }

        private TDataWrapper binDecode_UTF_STRING(ByteArray buffer)
        {
            return new TDataWrapper(TDataType.STRING, buffer.readString());
        }

        private ByteArray binEncode_BOOL(ByteArray buffer, bool val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.BOOL);
            newData.writeBool(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_BYTE(ByteArray buffer, byte val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.BYTE);
            newData.writeByte(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_BYTE_ARRAY(ByteArray buffer, byte[] val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.BYTE_ARRAY);
            newData.writeInt(val.Length);
            newData.writeBytes(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_DOUBLE(ByteArray buffer, double val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.DOUBLE);
            newData.writeDouble(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_FLOAT(ByteArray buffer, float val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.FLOAT);
            newData.writeFloat(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_INT(ByteArray buffer, int val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.INT);
            newData.writeInt(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_LONG(ByteArray buffer, long val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.LONG);
            newData.writeLong(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_NULL(ByteArray buffer)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.NULL);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_SHORT(ByteArray buffer, short val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.SHORT);
            newData.writeShort(val);
            return addData(buffer, newData);
        }

        private ByteArray binEncode_STRING(ByteArray buffer, string val)
        {
            ByteArray newData = new ByteArray();
            newData.writeByte((byte)TDataType.STRING);
            newData.writeString(val);
            return addData(buffer, newData);
        }

        private TDataWrapper decodeObject(ByteArray buffer)
        {
            TDataType type = (TDataType)Convert.ToInt32(buffer.readByte());
            switch (type)
            {
                case TDataType.NULL:
                    return binDecode_NULL(buffer);
                case TDataType.BOOL:
                    return binDecode_BOOL(buffer);
                case TDataType.BYTE:
                    return binDecode_BYTE(buffer);
                case TDataType.BYTE_ARRAY:
                    return binDecode_BYTE_ARRAY(buffer);
                case TDataType.SHORT:
                    return binDecode_SHORT(buffer);
                case TDataType.INT:
                    return binDecode_INT(buffer);
                case TDataType.LONG:
                    return binDecode_LONG(buffer);
                case TDataType.FLOAT:
                    return binDecode_FLOAT(buffer);
                case TDataType.DOUBLE:
                    return binDecode_DOUBLE(buffer);
                case TDataType.STRING:
                    return binDecode_UTF_STRING(buffer);
                case TDataType.TARRAY:
                    buffer.position--;
                    return new TDataWrapper(TDataType.TARRAY, decodeTArray(buffer));
            }
            if (type != TDataType.TOBJECT)
            {
                throw new Exception("Unknow DataType ID: " + type);
            }
            buffer.position--;
            ITObject mpObj = decodeTObject(buffer);
            object data = mpObj;
            return new TDataWrapper(TDataType.TOBJECT, data);
        }

        private ITArray decodeTArray(ByteArray buffer)
        {
            ITArray array = TArray.newInstance();
            TDataType type = (TDataType)Convert.ToInt32(buffer.readByte());
            if (type != TDataType.TARRAY)
            {
                throw new Exception(string.Concat("Invalid DataType. Expected: ", TDataType.TARRAY, ", found: ", type));
            }
            int num = buffer.readShort();
            if (num < 0)
            {
                throw new Exception("Can't decode Array. Size is negative: " + num);
            }
            for (int i = 0; i < num; i++)
            {
                TDataWrapper val = decodeObject(buffer);
                if (val == null)
                {
                    throw new Exception("Could not decode Array item at index: " + i);
                }
                array.add(val);
            }
            return array;
        }

        private ITObject decodeTObject(ByteArray buffer)
        {
            TObject obj2 = TObject.newInstance();
            byte num = buffer.readByte();
            if (num != Convert.ToByte(TDataType.TOBJECT))
            {
                throw new Exception(string.Concat("Invalid DataType. Expected: ", TDataType.TOBJECT, ", found: ", num));
            }
            int num2 = buffer.readShort();
            if (num2 < 0)
            {
                throw new Exception("Can't decode Object. Size is negative: " + num2);
            }
            for (int i = 0; i < num2; i++)
            {
                string key = buffer.readString();
                TDataWrapper val = decodeObject(buffer);
                if (val == null)
                {
                    throw new Exception("Could not decode value for Object with key: " + key);
                }
                obj2.put(key, val);
            }
            return obj2;
        }

        private ByteArray encodeObject(ByteArray buffer, TDataType typeId, object data)
        {
            switch (typeId)
            {
                case TDataType.NULL:
                    buffer = binEncode_NULL(buffer);
                    return buffer;
                case TDataType.BOOL:
                    buffer = binEncode_BOOL(buffer, (bool)data);
                    return buffer;
                case TDataType.BYTE:
                    buffer = binEncode_BYTE(buffer, (byte)data);
                    return buffer;
                case TDataType.SHORT:
                    buffer = binEncode_SHORT(buffer, (short)data);
                    return buffer;
                case TDataType.INT:
                    buffer = binEncode_INT(buffer, (int)data);
                    return buffer;
                case TDataType.LONG:
                    buffer = binEncode_LONG(buffer, (long)data);
                    return buffer;
                case TDataType.FLOAT:
                    buffer = binEncode_FLOAT(buffer, (float)data);
                    return buffer;
                case TDataType.DOUBLE:
                    buffer = binEncode_DOUBLE(buffer, (double)data);
                    return buffer;
                case TDataType.STRING:
                    buffer = binEncode_STRING(buffer, (string)data);
                    return buffer;
                case TDataType.TARRAY:
                    buffer = addData(buffer, array2Binary((ITArray)data));
                    return buffer;
                case TDataType.BYTE_ARRAY:
                    buffer = binEncode_BYTE_ARRAY(buffer, (byte[])data);
                    return buffer;
                case TDataType.TOBJECT:
                    buffer = addData(buffer, object2Binary((TObject)data));
                    return buffer;
            }
            throw new Exception("Unrecognized type in Object serialization: " + typeId);
        }

        private ByteArray obj2bin(ITObject obj, ByteArray buffer)
        {
            foreach (string str in obj.getKeys())
            {
                TDataWrapper data = obj.get(str);
                buffer.writeString(str);
                buffer = encodeObject(buffer, data.getTypeId(), data.getObject());
            }
            return buffer;
        }
    }
}

